package com.roadmap.client.module.statistic;

import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.Iterator;
import java.util.List;

import org.apache.log4j.Logger;

import com.roadmap.client.module.plan.IPlanProgress;
import com.roadmap.client.module.plan.PlanProgress;
import com.roadmap.common.BaseConst;
import com.roadmap.common.BaseUtility;
import com.roadmap.common.RdcLogger;
import com.roadmap.entities.LifePlan;
import com.roadmap.entities.LifeTask;
import com.roadmap.entities.Milestone;
import com.roadmap.exception.InvalidQueryException;
import com.roadmap.exception.InvalidValueException;
import com.roadmap.exception.NullObjectException;
import com.roadmap.exception.ServerException;
import com.roadmap.repo.db.DatabaseUtility;
import com.roadmap.repo.db.IDatabaseUtility;

public class StatisticUtil implements IStatisticUtil {
	private final static Logger logger = RdcLogger
			.getLogger(StatisticUtil.class);
	private final IDatabaseUtility _dbUtil = new DatabaseUtility();
	private final StringBuffer _querySql = new StringBuffer();

	// the goal's normal statuses are stored in this array
	private final static int[] _statusesOfGoal = {
			BaseConst.LIFEGOAL_STATUS_ALL,
			BaseConst.LIFEGOAL_STATUS_UNSCHEDULED,
			BaseConst.LIFEGOAL_STATUS_SCHEDULED,
			BaseConst.LIFEGOAL_STATUS_UNDERGOING,
			BaseConst.LIFEGOAL_STATUS_COMPLETED,
			BaseConst.LIFEGOAL_STATUS_CANCELED };

	// the amount of goals with different status will be stored in this array.
	// The first element will the amount of all goals
	private final int[] _amountOfGoalByStatus = new int[_statusesOfGoal.length];

	// the goal's normal statuses are stored in this array
	private final static int[] _statusesOfTask = {
			BaseConst.LIFETASK_STATUS_ALL, BaseConst.LIFETASK_STATUS_SCHEDULED,
			BaseConst.LIFETASK_STATUS_UNDERGOING,
			BaseConst.LIFETASK_STATUS_COMPLETED,
			BaseConst.LIFETASK_STATUS_CANCELED };

	// the amount of tasks with different status will be stored in this array.
	// The first element will be the amount of all tasks
	private final int[] _amountOfTaskByStatus = new int[_statusesOfTask.length];

	public List<ProgressPoint> getAcutalExecutingTrend(LifePlan plan)
			throws NullObjectException, InvalidQueryException,
			InvalidValueException, ServerException {
		if (plan == null) {
			NullObjectException noe = new NullObjectException();
			logger.error("Object plan can't be NULL", noe);
			throw noe;
		}
		else if (plan.getRealStartDate() == null) {
			logger.debug("the plan is not started yet");
			return null;
		}

		int interval = BaseConst.CHART_AXIS_X_POINTS;
		List<ProgressPoint> progressPoints = new ArrayList<ProgressPoint>();

		// If the plan is not completed, the real progress trend will only reach
		// current date
		Calendar calendar = Calendar.getInstance();
		if (plan.getRealEndDate() != null) {
			calendar.setTime(plan.getRealEndDate());
		}
		long endLg = calendar.getTimeInMillis();
		calendar.setTime(plan.getRealStartDate());
		long startLg = calendar.getTimeInMillis();
		long itLg = interval * BaseConst.DayToMillion;
		Date date = null;
		int prop = 0;
		long day = startLg;
		for (; day < endLg; day += itLg) {
			if (logger.isDebugEnabled()) {
				String msg = "starting to calculate the real progress on this date: "
						+ day;
				logger.debug(msg);
			}

			calendar.setTimeInMillis(day);
			date = calendar.getTime();
			prop = calculateProgressByDate(plan.getId(), date);
			progressPoints.add(new ProgressPoint(date, prop));

			if (logger.isDebugEnabled()) {
				logger.debug("actual progress: " + date + ", " + prop);
			}
		}

		// Calculate the progress of end day. When the end is current day, the
		// date will has hours and minutes, e.g. "2009-4-27 10:12:50". But the
		// date got from database is just like "2009-4-27". The hours and
		// minutes are "00:00" by default.
		if (day - endLg >= itLg) {
			calendar.setTimeInMillis(endLg);
			date = calendar.getTime();
			prop = calculateProgressByDate(plan.getId(), date);
			progressPoints.add(new ProgressPoint(date, prop));

			if (logger.isDebugEnabled()) {
				logger.debug("actual progress: " + date + ", " + prop);
			}
		}
		return progressPoints;
	}

	/*
	 * 1. verify which tasks belong to the plan and which executions belong to
	 * those tasks; 2. filter the executions by a specific date
	 */
	private int calculateProgressByDate(String planId, Date date)
			throws NullObjectException, InvalidQueryException, ServerException,
			InvalidValueException {
		String dateStr = BaseUtility.formatDate(date);

		// get all tasks of the plan
		StringBuffer criteria = new StringBuffer("planId='");
		criteria.append(planId);
		criteria.append("' and status<>");
		criteria.append(BaseConst.LIFETASK_STATUS_DELETED);
		List<LifeTask> tasks = (List<LifeTask>) _dbUtil.getEntitiesByCriteria(
				LifeTask.class, criteria.toString());

		// if the plan has no tasks, return 0
		if (tasks == null || tasks.size() == 0) {
			return 0;
		}

		int total = 0;
		StringBuffer sqlBuf = new StringBuffer(
				"select sum(proportionOfTask) from execution where taskId='");
		int startIndex = sqlBuf.length();
		for (int i = 0; i < tasks.size(); i++) {
			if (i != 0) {
				sqlBuf.delete(startIndex, sqlBuf.length());
			}
			LifeTask task = (LifeTask) tasks.get(i);
			sqlBuf.append(task.getId());
			sqlBuf.append("' and executeDate<='");
			sqlBuf.append(dateStr);
			sqlBuf.append("'");

			List list = _dbUtil.queryBySQL(sqlBuf.toString());
			if (list != null && list.size() > 0) {
				Object prop = list.get(0);
				total += (prop != null) ? (((BigDecimal) prop).intValue()
						* task.getProportionOfGoal() / 100) : 0;
			}
		}

		return total;
	}

	public int[] getGoalCounts(String aspirantId) throws InvalidQueryException,
			ServerException, InvalidValueException {
		if (aspirantId == null) {
			InvalidValueException ive = new InvalidValueException();
			logger.error("aspirant id can't be NULL", ive);
			throw ive;
		}

		cleanUpArray(_amountOfGoalByStatus);

		// calculate the amount of all goals belonging to the aspirant
		_amountOfGoalByStatus[BaseConst.LIFEGOAL_STATUS_ALL] = getGoalCountByStatus(
				aspirantId, BaseConst.LIFEGOAL_STATUS_ALL);
		if (logger.isDebugEnabled()) {
			logger.debug("Amount of all goals is: "
					+ _amountOfGoalByStatus[BaseConst.LIFEGOAL_STATUS_ALL]);
		}

		// calculate the number goals with different status. if the amount of
		// remaining goals with other statuses is 0, no need to continue
		// calculating
		int left = getGoalCountByStatus(aspirantId, _statusesOfGoal[0]);;
		for (int i = 1; i < _statusesOfGoal.length && left > 0; i++) {
			int status = _statusesOfGoal[i];
			_amountOfGoalByStatus[status] = getGoalCountByStatus(aspirantId,
					status);
			left = left - _amountOfGoalByStatus[status];

			if (logger.isDebugEnabled()) {
				logger.debug("Amount of goals with status " + status + " is: "
						+ _amountOfGoalByStatus[status]);
			}
		}

		return _amountOfGoalByStatus;
	}

	/*
	 * In case multiple threads call this method meanwhile, ensure whenever
	 * there will be only one thread using _querySql
	 */
	private int getGoalCountByStatus(String aspirantId, int status)
			throws NullObjectException, InvalidQueryException, ServerException {
		String sql = null;
		synchronized (_querySql) {
			// clean up the SQL statement
			_querySql.delete(0, _querySql.length());

			_querySql
					.append("select count(goalId) from lifeGoal where aspirantId='");
			_querySql.append(aspirantId + "'");
			if (status != BaseConst.LIFEGOAL_STATUS_ALL) {
				_querySql.append(" and status=");
				_querySql.append(status);
			}
			else {
				// "deleted" items are unseen for users
				_querySql.append(" and status<>");
				_querySql.append(BaseConst.LIFEGOAL_STATUS_DELETED);
			}

			sql = _querySql.toString();
			_querySql.notifyAll();
		}

		List list = _dbUtil.queryBySQL(sql);
		if (list != null && list.size() > 0) {
			Object obj = list.get(0);
			if (obj != null)
				return ((BigInteger) obj).intValue();
		}
		return 0;
	}

	/*
	 * the ideal progress will be measured by plan's milestones
	 */
	public List<ProgressPoint> getIdealExecutingTrend(LifePlan plan)
			throws NullObjectException, InvalidQueryException,
			InvalidValueException, ServerException {
		if (plan == null) {
			NullObjectException noe = new NullObjectException();
			logger.error("Object plan can't be NULL", noe);
			throw noe;
		}
		List<ProgressPoint> progressPoints = new ArrayList<ProgressPoint>();
		IPlanProgress planPro = new PlanProgress();
		List list = planPro.getAllMilestonesOfPlan(plan);
		if (list == null || list.size() == 0) {
			InvalidValueException ive = new InvalidValueException();
			logger.error("the plan has no milestones that is invalid.", ive);
		}
		
		progressPoints.add(new ProgressPoint(plan.getScheduledStartDate(), 0));

		Iterator iter = list.iterator();
		while (iter.hasNext()) {
			Milestone stone = (Milestone) iter.next();
			progressPoints.add(new ProgressPoint(stone.getStoneDate(), stone
					.getProportionOfPlan()));

			if (logger.isDebugEnabled()) {
				logger.debug("ideal progress: " + stone.getStoneDate() + ", "
						+ stone.getProportionOfPlan());
			}
		}
		return progressPoints;
	}

	public int[] getTaskCounts(String aspirantId) throws InvalidValueException,
			InvalidQueryException, ServerException {
		if (aspirantId == null) {
			InvalidValueException ive = new InvalidValueException();
			logger.error("aspirant id can't be NULL", ive);
			throw ive;
		}

		cleanUpArray(_amountOfTaskByStatus);

		// calculate the amount of all goals belonging to the aspirant
		_amountOfTaskByStatus[BaseConst.LIFETASK_STATUS_ALL] = getTaskCountByStatus(
				aspirantId, BaseConst.LIFETASK_STATUS_ALL);
		if (logger.isDebugEnabled()) {
			logger.debug("Amount of all tasks is: "
					+ _amountOfTaskByStatus[BaseConst.LIFETASK_STATUS_ALL]);
		}

		// calculate the number goals with different status. if the amount of
		// left goals with other status is 0, no need to continue calculating
		int left = _amountOfTaskByStatus[BaseConst.LIFETASK_STATUS_ALL];
		for (int i = 1; i < _statusesOfTask.length && left > 0; i++) {
			int status = _statusesOfTask[i];
			_amountOfTaskByStatus[status] = getTaskCountByStatus(aspirantId,
					status);
			left = left - _amountOfTaskByStatus[status];

			if (logger.isDebugEnabled()) {
				logger.debug("Amount of tasks with status " + status + " is: "
						+ _amountOfTaskByStatus[status]);
			}
		}

		return _amountOfTaskByStatus;
	}

	private int getTaskCountByStatus(String aspirantId, int status)
			throws NullObjectException, InvalidQueryException, ServerException {
		String sql = null;
		synchronized (_querySql) {
			// clean up the SQL statement
			_querySql.delete(0, _querySql.length());

			_querySql
					.append("select count(taskId) from lifeTask where aspirantId='");
			_querySql.append(aspirantId + "'");
			if (status != BaseConst.LIFETASK_STATUS_ALL) {
				_querySql.append(" and status=");
				_querySql.append(status);
			}
			else {
				// "deleted" items are unseen for users
				_querySql.append(" and status<>");
				_querySql.append(BaseConst.LIFETASK_STATUS_DELETED);
			}

			sql = _querySql.toString();
			_querySql.notifyAll();
		}

		List list = _dbUtil.queryBySQL(sql);
		if (list != null && list.size() > 0) {
			Object obj = list.get(0);
			if (obj != null){
				return ((BigInteger) obj).intValue();
			}
		}
		return 0;
	}

	public int[] getTaskCountsOfPlan(String planId)
			throws InvalidValueException, InvalidQueryException,
			ServerException {
		if (planId == null) {
			InvalidValueException ive = new InvalidValueException();
			logger.error("plan id can't be NULL", ive);
			throw ive;
		}

		cleanUpArray(_amountOfTaskByStatus);

		// calculate the amount of all goals belonging to the aspirant
		_amountOfTaskByStatus[BaseConst.LIFETASK_STATUS_ALL] = getTaskCountOfPlanByStatus(
				planId, BaseConst.LIFETASK_STATUS_ALL);
		if (logger.isDebugEnabled()) {
			logger.debug("Amount of all tasks is: "
					+ _amountOfTaskByStatus[BaseConst.LIFETASK_STATUS_ALL]);
		}

		// calculate the number goals with different status. if the amount of
		// left goals with other status is 0, no need to continue calculating
		int left = _amountOfTaskByStatus[BaseConst.LIFETASK_STATUS_ALL];
		for (int i = 1; i < _statusesOfTask.length && left > 0; i++) {
			int status = _statusesOfTask[i];
			_amountOfTaskByStatus[status] = getTaskCountOfPlanByStatus(planId,
					status);
			left = left - _amountOfTaskByStatus[status];

			if (logger.isDebugEnabled()) {
				logger.debug("Amount of tasks with status " + status + " is: "
						+ _amountOfTaskByStatus[status]);
			}
		}

		return _amountOfTaskByStatus;
	}

	private int getTaskCountOfPlanByStatus(String planId, int status)
			throws NullObjectException, InvalidQueryException, ServerException {
		String sql = null;
		synchronized (_querySql) {
			// clean up the SQL statement
			_querySql.delete(0, _querySql.length());

			_querySql
					.append("select count(taskId) from lifeTask where planId='");
			_querySql.append(planId + "'");
			if (status != BaseConst.LIFETASK_STATUS_ALL) {
				_querySql.append(" and status=");
				_querySql.append(status);
			}
			else {
				// "deleted" items are unseen for users
				_querySql.append(" and status<>");
				_querySql.append(BaseConst.LIFETASK_STATUS_DELETED);
			}

			sql = _querySql.toString();
			_querySql.notifyAll();
		}

		List list = _dbUtil.queryBySQL(sql);
		if (list != null && list.size() > 0) {
			Object obj = list.get(0);
			if (obj != null){
				return ((BigInteger) obj).intValue();
			}
		}
		return 0;
	}

	/**
	 * reset the value of each element in the array to 0, this ensures the array
	 * is clean before it is reused again.
	 */
	private void cleanUpArray(int[] array) {
		if (array == null)
			return;

		for (int i = 0; i < array.length; i++) {
			array[i] = 0;
		}
	}

}
